package com.yy.distributed.spring.aspect;

import com.yy.distributed.lock.DistributedLockCallback;
import com.yy.distributed.spring.annotation.DistributedLock;
import com.yy.distributed.lock.util.DLockUtil;
import com.yy.distributed.lock.DistributedLockTemplate;
import com.yy.distributed.spring.annotation.LockName;
import org.apache.commons.beanutils.PropertyUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * Write your description here...
 *
 * @author dinghuan
 * @date 2018/7/19
 */
@Aspect
@Component
public class DistributedLockAspect {
    private static final Logger logger = LoggerFactory.getLogger(DistributedLockAspect.class);

    @Autowired
    private DistributedLockTemplate lockTemplate;

    @Pointcut("@annotation(com.yy.distributed.spring.annotation.DistributedLock)")
    public void DistributedLockAspect() {
    }

    @Around(value = "DistributedLockAspect()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        //切点所在的类
        Class targetClass = pjp.getTarget().getClass();
        //使用了注解的方法
        String methodName = pjp.getSignature().getName();

        Class[] parameterTypes = ((MethodSignature) pjp.getSignature()).getMethod().getParameterTypes();

        Method method = targetClass.getMethod(methodName, parameterTypes);

        Object[] arguments = pjp.getArgs();

        final String lockName = getLockName(targetClass, method, arguments);

        logger.info("aspect do around... lockName: [{}]", lockName);

        return lock(pjp, method, lockName);
    }

    @AfterThrowing(value = "DistributedLockAspect()", throwing = "ex")
    public void afterThrowing(Throwable ex) {
        throw new RuntimeException(ex);
    }

    /**
     * 获取锁 key
     *
     * @param targetClass
     * @param method
     * @param args
     * @return
     */
    public String getLockName(Class targetClass, Method method, Object[] args) {
        Objects.requireNonNull(method);
        DistributedLock annotation = method.getAnnotation(DistributedLock.class);

        String lockName = annotation.lockName();

        if (isEmpty(lockName)) {
            if (args.length > 0) {
                Annotation[][] parameterAnnotations = method.getParameterAnnotations();

                int index = 0;
                for (Annotation[] annotations : parameterAnnotations) {
                    for (Annotation paramAnnotation : annotations) {
                        if (paramAnnotation instanceof LockName) {
                            lockName = String.valueOf(getParameterLockName(args[index], (LockName) paramAnnotation));
                            break;
                        }
                    }
                    index++;
                    if (isNotEmpty(lockName)) {
                        break;
                    }
                }
            }
        }
        if (isEmpty(lockName)) {
            lockName = DLockUtil.getClassMethodLockName(targetClass, method);
        }

        if (isNotEmpty(lockName)) {
            String preLockName = annotation.lockNamePre(),
                    postLockName = annotation.lockNamePost(),
                    separator = annotation.separator();

            StringBuilder lName = new StringBuilder();
            if (isNotEmpty(preLockName)) {
                lName.append(preLockName).append(separator);
            }
            lName.append(lockName);
            if (isNotEmpty(postLockName)) {
                lName.append(separator).append(postLockName);
            }

            lockName = lName.toString();

            return lockName;
        }

        throw new IllegalArgumentException("Can't get or generate lockName accurately!");
    }

    /**
     * 获取参数注解的lockName
     *
     * @param arg
     * @param lockNameAnnotation
     * @return
     */
    private Object getParameterLockName(Object arg, LockName lockNameAnnotation) {
        String property = lockNameAnnotation.property();
        if (isEmpty(property)) {
            return arg;
        } else {
            try {
                Object result = PropertyUtils.getProperty(arg, property);
                return result;
            } catch (NoSuchMethodException e) {
                throw new IllegalArgumentException(arg + "没有属性" + property + "或未实现get方法。", e);
            } catch (Exception e) {
                throw new RuntimeException("", e);
            }
        }
    }


    public Object lock(ProceedingJoinPoint pjp, Method method, final String lockName) {

        DistributedLock annotation = method.getAnnotation(DistributedLock.class);

        boolean fairLock = annotation.fairLock();

        boolean tryLock = annotation.tryLock();

        if (tryLock) {
            return tryLock(pjp, annotation, lockName, fairLock);
        } else {
            return lock(pjp, lockName, fairLock);
        }
    }

    public Object lock(final ProceedingJoinPoint pjp, final String lockName, boolean fairLock) {
        return lockTemplate.lock(new DistributedLockCallback<Object>() {
            @Override
            public Object process() {
                return proceed(pjp);
            }

            @Override
            public String getLockName() {
                return lockName;
            }
        }, fairLock);
    }

    public Object tryLock(final ProceedingJoinPoint pjp, DistributedLock annotation, final String lockName, boolean fairLock) {

        long waitTime = annotation.waitTime(),
                leaseTime = annotation.leaseTime();
        TimeUnit timeUnit = annotation.timeUnit();
        boolean release = annotation.release();

        if (leaseTime < 0) {
            throw new IllegalArgumentException("leaseTime must greater than 0 !");
        }

        return lockTemplate.tryLock(new DistributedLockCallback<Object>() {
            @Override
            public Object process() {
                return proceed(pjp);
            }

            @Override
            public String getLockName() {
                return lockName;
            }
        }, waitTime, leaseTime, timeUnit, fairLock, release);
    }

    public Object proceed(ProceedingJoinPoint pjp) {
        try {
            return pjp.proceed();
        } catch (Throwable throwable) {
            throw new RuntimeException(throwable);
        }
    }

    private boolean isEmpty(Object str) {
        return str == null || "".equals(str);
    }

    private boolean isNotEmpty(Object str) {
        return !isEmpty(str);
    }
}
